学习Java AOP的实现
AOP介绍
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
--摘自百度百科
通过这个介绍还不是很理解怎么办?什么是切面呀?
我的理解哈:你现在有一系列普通的执行方法,但是想在方法执行的前后加点东西,比如记录日志,那你不可能去再每个方法上都去加记录日志的语句,那这个时候AOP就来了,将这个记录日志作为切面切入到这些方法中,在平常自然调用这些方法时候同时执行这些切面。
AOP原理
切入的方法主要有在编译器修改源代码,在运行期字节码加载前修改字节码或字节码加载后动态创建代理类的字节码,下面借用别人总结的一个表:
本文主要是学习“动态AOP”以及“动态字节码生成” 这两种比较常用的实现方法
动态AOP-动态代理
这种实现方法最主要的就是Proxy.newProxyInstance
生成代理方法,其核心需要的是:
也就是一定需要接口
1 | public interface Student { |
同时将该接口进行一个实现1
2
3
4
5
6public class StudentImpl implements Student {
public void sayHello()
{
System.out.println("hello ,i am tom");
}
}
然后我们添加传说中的切面1
2
3
4
5
6
7
8
9/**
* 定义日志切面的接口
* @author yanyl
*
*/
public interface LogAspect {
void before();//方法调用前执行
void after();//方法调用后执行
}
随便实现一个切面1
2
3
4
5
6
7
8
9
10
11public class PrintLogAspect implements LogAspect {
public void before()
{
System.out.println("Log:before invoke");
}
public void after()
{
System.out.println("after invoke");
}
}
好,我们的目标就是将切面LogAspect
切入到Student
这个接口中。
现在我们现在创建代理之前先创建一个InvocationHandler
的子类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37/**
* 这就是调用打印日志的切面
* @author yanyl
*
*/
public class LogInvocationHandler implements InvocationHandler {
private final Object target;//这个就是调用的目标类
private final List<LogAspect> aspectList=new ArrayList<LogAspect>();//所指定的切面方法
public LogInvocationHandler(Object instance)
{
this.target=instance;
}
public void addAspect(LogAspect logAspect)
{
this.aspectList.add(logAspect);
}
/**
* 在这个切面调用原方法执行
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
for(int i=0;i<aspectList.size();i++)
aspectList.get(i).before();//调用方法执行前到方法
Object ret=method.invoke(target, args);
for(int i=aspectList.size()-1;i>=0;i--)
aspectList.get(i).before();//这里是方法执行后的方法
return ret;
}
}
好了我们再实现一个比较简单的日志功能个:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* 则是一个非常简单的代理工厂,主要就是创建新的实例
* @author yanyl
*
*/
public class SimpleProxyFactory {
private SimpleProxyFactory(){};//不公开构造方法
public Object newInstance(Object instance,LogAspect logAspect)
{
LogInvocationHandler h=new LogInvocationHandler(instance);
h.addAspect(logAspect);
//这里是直接返回创建的代理类
return Proxy.newProxyInstance(instance.getClass().getClassLoader(),
instance.getClass().getInterfaces(), h);
}
}
最后再来看main方法怎么启动:1
2
3
4
5
6
7
8public static void main(String[] args) {
//这里创建一个打印的切面
LogAspect logAspect=new PrintLogAspect();
//使用简单的代理工厂生产具体的接口
Student s=(Student)SimpleProxyFactory.newInstance(new StudentImpl(), logAspect);
//接口调用方法 可以发现已经带上了需要切入的面
s.sayHello();
}
最终的输结果:
Log:before invoke
hello ,i am tom
Log:before invoke
可以发现打印日志已经成功切入。^_^
还是蛮神奇的,竟然直接使用原来接口的方法就能自动切入额外打印功能,那么这个代理类是如何工作的呢?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34//模拟一下代理类公的工作
public class ProxyMock implements Student {
private LogInvocationHandler h;
public ProxyMock(LogInvocationHandler h)
{
this.h=h;
}
@Override
public void sayHello() {
try
{
Method m=h.getTarget().getClass().getMethod("sayHello", null);
h.invoke(this, m,null);//最终调用handler.invoke 来执行
}catch(Throwable e)
{
}
}
public static void main(String[] args) {
LogInvocationHandler lh=new LogInvocationHandler(new StudentImpl());
lh.addAspect(new PrintLogAspect());
Student student=new ProxyMock(lh);//模拟的代理类
student.sayHello();
}
}
上面是模拟的代理类,在sayHello
方法里面使用反射最终还是调用了h.invoke
方法,那这样思路就比较清晰了
- 使用上面代理类的时候别忘了在
LogInvocationHandler
类上添加getTarget
的方法- 如果需要知道如何用代码来实现上述过程 请看JDK中的
Proxy
源码或者下面参考的文章也有介绍
使用动态代理实现AOP
问题比较方便,也相当灵活,但是有以下几个缺点:
- 一定需要定义接口
- 实现是使用反射的,所以带来性能影响
- 反射生成的文件可能会照成频繁的Major GC
动态字节码生成-cglib技术
cglib
是一个强大的高效的字节码生成类库,可以在运行期间扩展Java类或者实现java接口,那么按照上述的动态代理技术,使用cglib
就不必在去反射,而是直接基于需要代理的类直接取生成一个扩展的字节码即可,这样的话这种方式的切入逻辑为:
现在还是来用LogAspect
来进行切入 但是这次切入是切入到具体的类StudentImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/**
* 使用cglib来实现AOP
* @author yanyl
*
*/
public class CgLibTest {
public static void main(String[] args)
{
Enhancer enhancer=new Enhancer();
enhancer.setSuperclass(StudentImpl.class);//设置需要继承的父类 也就是需要切入的类
enhancer.setCallback(new LogMethodInterceptor(new PrintLogAspect()));//设置拦截器 这里顺带设置切面
enhancer.setUseCache(true);
StudentImpl studentImpl=(StudentImpl)enhancer.create();//创建切入的子类
studentImpl.sayHello();//这里直接对类进行了切入 使用原来的类就可以了
}
/**
* 日志方法拦截器
* 这里的MethodInterceptor是继承net.sf.cglib.proxy.Callback的
* @author yanyl
*
*/
public static class LogMethodInterceptor implements MethodInterceptor{
private LogAspect logAspect;
public LogMethodInterceptor(LogAspect logAspect)
{
this.logAspect=logAspect;
}
@Override
public Object intercept(Object instance, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
Object ret=null;
if(instance!=null && methodProxy!=null)
{
logAspect.before();//进行切入
ret=methodProxy.invokeSuper(instance, args);//注意,这里一定是要执行这个proxy的method 并且是invokeSuper
logAspect.after();
}
return ret;
}
}
}
最终的输出为:
Log:before invoke
hello ,i am tom
after invoke
上面的代码演示了如何使用cglib来进行AOP
的实现,可以发现使用cglib不需要提前创建接口,更加灵活,并且它是会动态生成相应的字节码,比原来的动态代理更加高效。
注意:这个实例需要引用
asm.jar
以及同时引用的cglib最好使用cglib-nodep.jar
防止冲突
AOP的作用
AOP
可以做好多事情了,比如:
- 性能监控,在方法调用前后记录调用时间,方法执行太长或超时报警。 赞
- 缓存代理,缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。 赞,妙
- 软件破解,使用AOP修改软件的验证类的判断逻辑。
- 记录日志,在方法执行前后记录系统日志。 本文的介绍就是用于日志的记录
- 工作流系统,工作流系统需要将业务代码和流程引擎代码混合在一起执行,那么我们可以使用AOP将其分离,并动态挂接业务。
- 权限验证,方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,由业务代码捕捉。
参考
关于
Spring
的AOP
看参考把,不再多说,本来本文绝大部分就是来自参考,自己组织了一下并且实际操练而已-_-
本作品采用[知识共享署名-非商业性使用-相同方式共享 2.5]中国大陆许可协议进行许可,我的博客欢迎复制共享,但在同时,希望保留我的署名权